/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
@file:JvmMultifileClass
@file:JvmName("RoomDatabaseKt")

package androidx.room

import androidx.annotation.RestrictTo
import androidx.room.concurrent.CloseBarrier
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.SQLiteDriver
import androidx.sqlite.SQLiteException
import kotlin.coroutines.CoroutineContext
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.reflect.KClass
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext

/**
 * Base class for all Room databases. All classes that are annotated with [Database] must extend
 * this class.
 *
 * RoomDatabase provides direct access to the underlying database implementation but you should
 * prefer using [Dao] classes.
 *
 * @see Database
 */
expect abstract class RoomDatabase() {

    /**
     * The invalidation tracker for this database.
     *
     * You can use the invalidation tracker to get notified when certain tables in the database are
     * modified.
     *
     * @return The invalidation tracker for the database.
     */
    val invalidationTracker: InvalidationTracker

    /**
     * A barrier that prevents the database from closing while the [InvalidationTracker] is using
     * the database asynchronously.
     *
     * @return The barrier for [close].
     */
    internal val closeBarrier: CloseBarrier

    /**
     * Called by Room when it is initialized.
     *
     * @param configuration The database configuration.
     * @throws IllegalArgumentException if initialization fails.
     */
    internal fun init(configuration: DatabaseConfiguration)

    /**
     * Creates a connection manager to manage database connection. Note that this method is called
     * when the [RoomDatabase] is initialized.
     *
     * @param configuration The database configuration
     * @return A new connection manager
     */
    internal fun createConnectionManager(
        configuration: DatabaseConfiguration
    ): RoomConnectionManager

    /**
     * Creates a delegate to configure and initialize the database when it is being opened.
     *
     * An implementation of this function is generated by the Room processor. Note that this method
     * is called when the [RoomDatabase] is initialized.
     *
     * @return A new delegate to be used while opening the database
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    protected open fun createOpenDelegate(): RoomOpenDelegateMarker

    /**
     * Creates the invalidation tracker
     *
     * An implementation of this function is generated by the Room processor. Note that this method
     * is called when the [RoomDatabase] is initialized.
     *
     * @return A new invalidation tracker.
     */
    protected abstract fun createInvalidationTracker(): InvalidationTracker

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun getCoroutineScope(): CoroutineScope

    /**
     * Returns a Set of required [AutoMigrationSpec] classes.
     *
     * An implementation of this function is generated by the Room processor. Note that this method
     * is called when the [RoomDatabase] is initialized.
     *
     * @return Creates a set that will include the classes of all required auto migration specs for
     *   this database.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    open fun getRequiredAutoMigrationSpecClasses(): Set<KClass<out AutoMigrationSpec>>

    /**
     * Returns a list of automatic [Migration]s that have been generated.
     *
     * An implementation of this function is generated by the Room processor. Note that this method
     * is called when the [RoomDatabase] is initialized.
     *
     * @param autoMigrationSpecs the provided specs needed by certain migrations.
     * @return A list of migration instances each of which is a generated 'auto migration'.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    open fun createAutoMigrations(
        autoMigrationSpecs: Map<KClass<out AutoMigrationSpec>, AutoMigrationSpec>
    ): List<Migration>

    /**
     * Gets the instance of the given type converter class.
     *
     * This method should only be called by the generated DAO implementations.
     *
     * @param klass The Type Converter class.
     * @param T The type of the expected Type Converter subclass.
     * @return An instance of T.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun <T : Any> getTypeConverter(klass: KClass<T>): T

    /**
     * Adds a provided type converter to be used in the database DAOs.
     *
     * @param kclass the class of the type converter
     * @param converter an instance of the converter
     */
    internal fun addTypeConverter(kclass: KClass<*>, converter: Any)

    /**
     * Returns a Map of String -> List&lt;KClass&gt; where each entry has the `key` as the DAO name
     * and `value` as the list of type converter classes that are necessary for the database to
     * function.
     *
     * An implementation of this function is generated by the Room processor. Note that this method
     * is called when the [RoomDatabase] is initialized.
     *
     * @return A map that will include all required type converters for this database.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    protected open fun getRequiredTypeConverterClasses(): Map<KClass<*>, List<KClass<*>>>

    /** Property delegate of [getRequiredTypeConverterClasses] for common ext functionality. */
    internal val requiredTypeConverterClassesMap: Map<KClass<*>, List<KClass<*>>>

    /**
     * Initialize invalidation tracker. Note that this method is called when the [RoomDatabase] is
     * initialized and opens a database connection.
     *
     * @param connection The database connection.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    protected fun internalInitInvalidationTracker(connection: SQLiteConnection)

    /**
     * Closes the database.
     *
     * Once a [RoomDatabase] is closed it should no longer be used.
     */
    fun close()

    /**
     * Use a connection to perform database operations.
     *
     * This function is for internal access to the pool, it is an unconfined coroutine function to
     * be used by Room generated code paths. For the public version see [useReaderConnection] and
     * [useWriterConnection].
     */
    internal suspend fun <R> useConnection(isReadOnly: Boolean, block: suspend (Transactor) -> R): R

    /**
     * Journal modes for SQLite database.
     *
     * @see Builder#setJournalMode
     */
    enum class JournalMode {
        /** Truncate journal mode. */
        TRUNCATE,

        /** Write-Ahead Logging mode. */
        WRITE_AHEAD_LOGGING
    }

    /**
     * Builder for [RoomDatabase].
     *
     * @param T The type of the abstract database class.
     */
    class Builder<T : RoomDatabase> {
        /**
         * Sets the [SQLiteDriver] implementation to be used by Room to open database connections.
         *
         * @param driver The driver
         * @return This builder instance.
         */
        fun setDriver(driver: SQLiteDriver): Builder<T>

        /**
         * Adds a migration to the builder.
         *
         * Each [Migration] has a start and end versions and Room runs these migrations to bring the
         * database to the latest version.
         *
         * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
         * going from version 3 to 5 without going to version 4). If Room opens a database at
         * version 3 and latest version is >= 5, Room will use the migration object that can migrate
         * from 3 to 5 instead of 3 to 4 and 4 to 5.
         *
         * @param migrations The migration objects that modify the database schema with the
         *   necessary changes for a version change.
         * @return This builder instance.
         */
        fun addMigrations(vararg migrations: Migration): Builder<T>

        /**
         * Adds an auto migration spec instance to the builder.
         *
         * @param autoMigrationSpec The auto migration object that is annotated with
         *   [ProvidedAutoMigrationSpec] and is declared in an [AutoMigration] annotation.
         * @return This builder instance.
         */
        fun addAutoMigrationSpec(autoMigrationSpec: AutoMigrationSpec): Builder<T>

        /**
         * Allows Room to destructively recreate database tables if [Migration]s that would migrate
         * old database schemas to the latest schema version are not found.
         *
         * When the database version on the device does not match the latest schema version, Room
         * runs necessary [Migration]s on the database. If it cannot find the set of [Migration]s
         * that will bring the database to the current version, it will throw an
         * [IllegalStateException]. You can call this method to change this behavior to re-create
         * the database tables instead of crashing.
         *
         * To let Room fallback to destructive migration only during a schema downgrade then use
         * [fallbackToDestructiveMigrationOnDowngrade].
         *
         * @param dropAllTables Set to `true` if all tables should be dropped during destructive
         *   migration including those not managed by Room. Recommended value is `true` as otherwise
         *   Room could leave obsolete data when table names or existence changes between versions.
         * @return This builder instance.
         */
        fun fallbackToDestructiveMigration(dropAllTables: Boolean): Builder<T>

        /**
         * Allows Room to destructively recreate database tables if [Migration]s are not available
         * when downgrading to old schema versions.
         *
         * For details, see [Builder.fallbackToDestructiveMigration].
         *
         * @param dropAllTables Set to `true` if all tables should be dropped during destructive
         *   migration including those not managed by Room. Recommended value is `true` as otherwise
         *   Room could leave obsolete data when table names or existence changes between versions.
         * @return This builder instance.
         */
        fun fallbackToDestructiveMigrationOnDowngrade(dropAllTables: Boolean): Builder<T>

        /**
         * Informs Room that it is allowed to destructively recreate database tables from specific
         * starting schema versions.
         *
         * This functionality is the same [fallbackToDestructiveMigration], except that this method
         * allows the specification of a set of schema versions for which destructive recreation is
         * allowed.
         *
         * Using this method is preferable to [fallbackToDestructiveMigration] if you want to allow
         * destructive migrations from some schema versions while still taking advantage of
         * exceptions being thrown due to unintentionally missing migrations.
         *
         * Note: No versions passed to this method may also exist as either starting or ending
         * versions in the [Migration]s provided via [addMigrations]. If a version passed to this
         * method is found as a starting or ending version in a Migration, an exception will be
         * thrown.
         *
         * @param dropAllTables Set to `true` if all tables should be dropped during destructive
         *   migration including those not managed by Room. Recommended value is `true` as otherwise
         *   Room could leave obsolete data when table names or existence changes between versions.
         * @param startVersions The set of schema versions from which Room should use a destructive
         *   migration.
         * @return This builder instance.
         */
        fun fallbackToDestructiveMigrationFrom(
            dropAllTables: Boolean,
            vararg startVersions: Int
        ): Builder<T>

        /**
         * Adds a type converter instance to the builder.
         *
         * @param typeConverter The converter instance that is annotated with
         *   [ProvidedTypeConverter].
         * @return This builder instance.
         */
        fun addTypeConverter(typeConverter: Any): Builder<T>

        /**
         * Sets the journal mode for this database.
         *
         * The value is ignored if the builder is for an 'in-memory database'. The journal mode
         * should be consistent across multiple instances of [RoomDatabase] for a single SQLite
         * database file.
         *
         * The default value is [JournalMode.WRITE_AHEAD_LOGGING].
         *
         * @param journalMode The journal mode.
         * @return This builder instance.
         */
        fun setJournalMode(journalMode: JournalMode): Builder<T>

        /**
         * Sets the [CoroutineContext] that will be used to execute all asynchronous queries and
         * tasks, such as `Flow` emissions and [InvalidationTracker] notifications.
         *
         * If no [CoroutineDispatcher] is present in the [context] then this function will throw an
         * [IllegalArgumentException]
         *
         * @param context The context
         * @return This [Builder] instance
         * @throws IllegalArgumentException if the [context] has no [CoroutineDispatcher]
         */
        fun setQueryCoroutineContext(context: CoroutineContext): Builder<T>

        /**
         * Adds a [Callback] to this database.
         *
         * @param callback The callback.
         * @return This builder instance.
         */
        fun addCallback(callback: Callback): Builder<T>

        /**
         * Creates the database and initializes it.
         *
         * @return A new database instance.
         * @throws IllegalArgumentException if the builder was misconfigured.
         */
        fun build(): T
    }

    /**
     * A container to hold migrations. It also allows querying its contents to find migrations
     * between two versions.
     */
    class MigrationContainer() {
        /**
         * Returns the map of available migrations where the key is the start version of the
         * migration, and the value is a map of (end version -> Migration).
         *
         * @return Map of migrations keyed by the start version
         */
        fun getMigrations(): Map<Int, Map<Int, Migration>>

        /**
         * Adds the given migrations to the list of available migrations. If 2 migrations have the
         * same start-end versions, the latter migration overrides the previous one.
         *
         * @param migrations List of available migrations.
         */
        fun addMigrations(migrations: List<Migration>)

        /**
         * Add a [Migration] to the container. If the container already has a migration with the
         * same start-end versions then it will be overwritten.
         *
         * @param migration the migration to add.
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun addMigration(migration: Migration)

        /**
         * Indicates if the given migration is contained within the [MigrationContainer] based on
         * its start-end versions.
         *
         * @param startVersion Start version of the migration.
         * @param endVersion End version of the migration
         * @return True if it contains a migration with the same start-end version, false otherwise.
         */
        fun contains(startVersion: Int, endVersion: Int): Boolean

        /**
         * Returns a pair corresponding to an entry in the map of available migrations whose key is
         * [migrationStart] and its sorted keys in ascending order.
         */
        internal fun getSortedNodes(migrationStart: Int): Pair<Map<Int, Migration>, Iterable<Int>>?

        /**
         * Returns a pair corresponding to an entry in the map of available migrations whose key is
         * [migrationStart] and its sorted keys in descending order.
         */
        internal fun getSortedDescendingNodes(
            migrationStart: Int
        ): Pair<Map<Int, Migration>, Iterable<Int>>?
    }

    /** Callback for [RoomDatabase] */
    abstract class Callback() {
        /**
         * Called when the database is created for the first time.
         *
         * This function called after all the tables are created.
         *
         * @param connection The database connection.
         */
        open fun onCreate(connection: SQLiteConnection)

        /**
         * Called after the database was destructively migrated.
         *
         * @param connection The database connection.
         */
        open fun onDestructiveMigration(connection: SQLiteConnection)

        /**
         * Called when the database has been opened.
         *
         * @param connection The database connection.
         */
        open fun onOpen(connection: SQLiteConnection)
    }
}

/**
 * Acquires a READ connection, suspending while waiting if none is available and then calling the
 * [block] to use the connection once it is acquired. A [RoomDatabase] will have one or more READ
 * connections. The connection to use in the [block] is an instance of [Transactor] that provides
 * the capabilities for performing nested transactions.
 *
 * Using the connection after [block] completes is prohibited.
 *
 * The connection will be confined to the coroutine on which [block] executes, attempting to use the
 * connection from a different coroutine will result in an error.
 *
 * If the current coroutine calling this function already has a confined connection, then that
 * connection is used.
 *
 * A connection is a limited resource and should not be held for more than it is needed. The best
 * practice in using connections is to avoid executing long-running computations within the [block].
 * If a caller has to wait too long to acquire a connection a [SQLiteException] will be thrown due
 * to a timeout.
 *
 * @param block The code to use the connection.
 * @throws SQLiteException when the database is closed or a thread confined connection needs to be
 *   upgraded or there is a timeout acquiring a connection.
 * @see [useWriterConnection]
 */
suspend fun <R> RoomDatabase.useReaderConnection(block: suspend (Transactor) -> R): R =
    withContext(getCoroutineScope().coroutineContext) { useConnection(isReadOnly = true, block) }

/**
 * Acquires a WRITE connection, suspending while waiting if none is available and then calling the
 * [block] to use the connection once it is acquired. A [RoomDatabase] will have only one WRITE
 * connection. The connection to use in the [block] is an instance of [Transactor] that provides the
 * capabilities for performing nested transactions.
 *
 * Using the connection after [block] completes is prohibited.
 *
 * The connection will be confined to the coroutine on which [block] executes, attempting to use the
 * connection from a different coroutine will result in an error.
 *
 * If the current coroutine calling this function already has a confined connection, then that
 * connection is used as long as it isn't required to be upgraded to a writer. If an upgrade is
 * required then a [SQLiteException] is thrown.
 *
 * A connection is a limited resource and should not be held for more than it is needed. The best
 * practice in using connections is to avoid executing long-running computations within the [block].
 * If a caller has to wait too long to acquire a connection a [SQLiteException] will be thrown due
 * to a timeout.
 *
 * @param block The code to use the connection.
 * @throws SQLiteException when the database is closed or a thread confined connection needs to be
 *   upgraded or there is a timeout acquiring a connection.
 * @see [useReaderConnection]
 */
suspend fun <R> RoomDatabase.useWriterConnection(block: suspend (Transactor) -> R): R =
    withContext(getCoroutineScope().coroutineContext) { useConnection(isReadOnly = false, block) }

/**
 * Validates that no added migration start or end are also marked as fallback to destructive
 * migration from.
 */
internal fun validateMigrationsNotRequired(
    migrationStartAndEndVersions: Set<Int>,
    migrationsNotRequiredFrom: Set<Int>
) {
    if (migrationStartAndEndVersions.isNotEmpty()) {
        for (version in migrationStartAndEndVersions) {
            require(!migrationsNotRequiredFrom.contains(version)) {
                "Inconsistency detected. A Migration was supplied to addMigration() that has a " +
                    "start or end version equal to a start version supplied to " +
                    "fallbackToDestructiveMigrationFrom(). Start version is: $version"
            }
        }
    }
}

internal fun RoomDatabase.validateAutoMigrations(configuration: DatabaseConfiguration) {
    val autoMigrationSpecs = mutableMapOf<KClass<out AutoMigrationSpec>, AutoMigrationSpec>()
    val requiredAutoMigrationSpecs = getRequiredAutoMigrationSpecClasses()
    val usedSpecs = BooleanArray(requiredAutoMigrationSpecs.size)
    for (spec in requiredAutoMigrationSpecs) {
        var foundIndex = -1
        for (providedIndex in configuration.autoMigrationSpecs.indices.reversed()) {
            val provided: Any = configuration.autoMigrationSpecs[providedIndex]
            if (spec.isInstance(provided)) {
                foundIndex = providedIndex
                usedSpecs[foundIndex] = true
                break
            }
        }
        require(foundIndex >= 0) {
            "A required auto migration spec (${spec.qualifiedName}) is missing in the " +
                "database configuration."
        }
        autoMigrationSpecs[spec] = configuration.autoMigrationSpecs[foundIndex]
    }
    for (providedIndex in configuration.autoMigrationSpecs.indices.reversed()) {
        require(providedIndex < usedSpecs.size && usedSpecs[providedIndex]) {
            "Unexpected auto migration specs found. " +
                "Annotate AutoMigrationSpec implementation with " +
                "@ProvidedAutoMigrationSpec annotation or remove this spec from the " +
                "builder."
        }
    }
    val autoMigrations = createAutoMigrations(autoMigrationSpecs)
    for (autoMigration in autoMigrations) {
        val migrationExists =
            configuration.migrationContainer.contains(
                autoMigration.startVersion,
                autoMigration.endVersion
            )
        if (!migrationExists) {
            configuration.migrationContainer.addMigration(autoMigration)
        }
    }
}

internal fun RoomDatabase.validateTypeConverters(configuration: DatabaseConfiguration) {
    val requiredFactories = this.requiredTypeConverterClassesMap
    // Indices for each converter on whether it is used or not so that we can throw an exception
    // if developer provides an unused converter. It is not necessarily an error but likely
    // to be because why would developer add a converter if it won't be used?
    val used = BooleanArray(requiredFactories.size)
    requiredFactories.forEach { (daoName, converters) ->
        for (converter in converters) {
            var foundIndex = -1
            // traverse provided converters in reverse so that newer one overrides
            for (providedIndex in configuration.typeConverters.indices.reversed()) {
                val provided = configuration.typeConverters[providedIndex]
                if (converter.isInstance(provided)) {
                    foundIndex = providedIndex
                    used[foundIndex] = true
                    break
                }
            }
            require(foundIndex >= 0) {
                "A required type converter (${converter.qualifiedName}) for" +
                    " ${daoName.qualifiedName} is missing in the database configuration."
            }
            addTypeConverter(converter, configuration.typeConverters[foundIndex])
        }
    }
    // now, make sure all provided factories are used
    for (providedIndex in configuration.typeConverters.indices.reversed()) {
        if (!used[providedIndex]) {
            val converter = configuration.typeConverters[providedIndex]
            throw IllegalArgumentException(
                "Unexpected type converter $converter. " +
                    "Annotate TypeConverter class with @ProvidedTypeConverter annotation " +
                    "or remove this converter from the builder."
            )
        }
    }
}
